feat(opencode-v2): OpenCode 2.x host adapter (reuses the existing core) - #13
feat(opencode-v2): OpenCode 2.x host adapter (reuses the existing core)#13Vladimir-Human wants to merge 5 commits into
Conversation
OpenCode 2.x replaced the plugin surface the 1.x package relies on: there is no
fetch() patch and no TUI sidebar, and providers are declared with native packages
in config. packages/opencode declares engines.opencode '>=1.17.13 <2', so 2.x
users currently have no Antigravity access.
This package is the missing host adapter. All logic still comes from
@cortexkit/antigravity-auth-core: OAuth, transport, account pool, quota and the
model registry are untouched.
- session.hook('http.request') reroutes Antigravity model calls to a loopback
server owned by the plugin, which performs the real request through
fetchWithAgyCliTransport so the agy wire format and proxy support survive
- account selection via getCurrentOrNextForFamily (hybrid), refreshAntigravityToken,
ensureProjectContext, agent envelope from buildAgyAgentRequestMetadata plus
orderAgyRequestPayloadInPlace, endpoint fallback list
- 429/403 cools the account down and rotates, 401 forces a refresh, an
empty-candidate STOP is retried three times
- the native @opencode-ai/ai/providers/google package stays the codec, so image,
PDF and tool-call handling comes from the host
- normalizeGeminiTools with moveNumericConstraintsToDescription for gpt-* models,
which fixes 400 INVALID_ARGUMENT on GPT-OSS tool schemas
- stream frames are normalised for the strict native Gemini event schema
(GPT-OSS opens a turn without parts, Claude may use role assistant)
- antigravity_read_document tool, because the 2.x CLI drops PDF attachments
- generated images are written to the data dir and announced as text
- logs carry account indexes only: no prompts, tokens or e-mail addresses
There was a problem hiding this comment.
1 issue found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode-v2/package.json">
<violation number="1" location="packages/opencode-v2/package.json:30">
P3: This new package ships no `scripts` and is not wired into the root workspace scripts: root `build`/`typecheck`/`test` cover only `packages/core`, `packages/opencode`, and `packages/pi`. As a result nothing builds, lints, or tests the opencode-v2 source in CI, even though the `packages/*` workspace glob pulls it into `bun install`. If this stays in the monorepo, add at least a `lint`/`build`/`test` script and reference it from the root; if it is meant to be a standalone package, that is fine, but the decision should be explicit rather than leaving a workspace member with zero CI coverage.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @@ -0,0 +1,45 @@ | |||
| { | |||
There was a problem hiding this comment.
P3: This new package ships no scripts and is not wired into the root workspace scripts: root build/typecheck/test cover only packages/core, packages/opencode, and packages/pi. As a result nothing builds, lints, or tests the opencode-v2 source in CI, even though the packages/* workspace glob pulls it into bun install. If this stays in the monorepo, add at least a lint/build/test script and reference it from the root; if it is meant to be a standalone package, that is fine, but the decision should be explicit rather than leaving a workspace member with zero CI coverage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode-v2/package.json, line 30:
<comment>This new package ships no `scripts` and is not wired into the root workspace scripts: root `build`/`typecheck`/`test` cover only `packages/core`, `packages/opencode`, and `packages/pi`. As a result nothing builds, lints, or tests the opencode-v2 source in CI, even though the `packages/*` workspace glob pulls it into `bun install`. If this stays in the monorepo, add at least a `lint`/`build`/`test` script and reference it from the root; if it is meant to be a standalone package, that is fine, but the decision should be explicit rather than leaving a workspace member with zero CI coverage.</comment>
<file context>
@@ -0,0 +1,45 @@
+ "node": ">=20.0.0",
+ "opencode": ">=2 <3"
+ },
+ "main": "./src/plugin.mjs",
+ "exports": {
+ ".": "./src/plugin.mjs",
</file context>
There was a problem hiding this comment.
Fixed in 0660034: the package ships lint/format:check/typecheck/test scripts plus a smoke test (packages/opencode-v2/test/plugin.test.mjs), and is wired into the root typecheck (node --check) and test chains. bun.lock is regenerated. Note: this package is plain ESM by design — the typecheck step is node --check rather than tsc.
- P1: allow only one forced OAuth refresh per account/request; a repeated 401 now excludes the account and continues pool selection instead of looping - P2: mark request execution complete after a terminal stream, a collected tool response or a non-stream answer, so follow-up tool-result requests keep the core's last_execution_id trajectory metadata - P2: answer non-streaming generateContent calls with a single JSON response (frames are merged) instead of an SSE body the host cannot parse - P2: restrict antigravity_read_document to the user's home directory by default, block well-known credential/secret paths always, and allow narrowing via the readDocumentRoots plugin option; README documents the prompt-injection/exfiltration risk - P2: OAuth callback page only reports that the authorization was received; the final result is reported by OpenCode after exchange + persistence - P2: fix the Install instructions (the adapter package, not just the core) and explain how the plugin path should be set - P3: add the package to the release publish matrix and fix the package-count references in the root README - P3: add lint/format/typecheck/test scripts, a smoke test and wire the package into the root typecheck/test chains; update bun.lock
There was a problem hiding this comment.
1 issue found across 12 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/release.yml">
<violation number="1" location=".github/workflows/release.yml:82">
P2: Adding `@cortexkit/opencode-v2-antigravity-auth` to the publish matrix breaks the release because its version is never synchronized. The workflow's `Sync version` step runs `scripts/version-sync.mjs <tag-version>`, whose `packageJsonPaths` array only covers `packages/{core,opencode,pi}/package.json` and omits `packages/opencode-v2/package.json`. So opencode-v2 always publishes its hardcoded `2.1.0`, which diverges from the release tag and, on the second release, collides with the already-published version and fails the `npm publish` job (which blocks the downstream `github-release`). Add the opencode-v2 path to `version-sync.mjs` alongside this matrix entry.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| workspace: "@cortexkit/opencode-antigravity-auth" | ||
| - package: "@cortexkit/pi-antigravity-auth" | ||
| workspace: "@cortexkit/pi-antigravity-auth" | ||
| - package: "@cortexkit/opencode-v2-antigravity-auth" |
There was a problem hiding this comment.
P2: Adding @cortexkit/opencode-v2-antigravity-auth to the publish matrix breaks the release because its version is never synchronized. The workflow's Sync version step runs scripts/version-sync.mjs <tag-version>, whose packageJsonPaths array only covers packages/{core,opencode,pi}/package.json and omits packages/opencode-v2/package.json. So opencode-v2 always publishes its hardcoded 2.1.0, which diverges from the release tag and, on the second release, collides with the already-published version and fails the npm publish job (which blocks the downstream github-release). Add the opencode-v2 path to version-sync.mjs alongside this matrix entry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 82:
<comment>Adding `@cortexkit/opencode-v2-antigravity-auth` to the publish matrix breaks the release because its version is never synchronized. The workflow's `Sync version` step runs `scripts/version-sync.mjs <tag-version>`, whose `packageJsonPaths` array only covers `packages/{core,opencode,pi}/package.json` and omits `packages/opencode-v2/package.json`. So opencode-v2 always publishes its hardcoded `2.1.0`, which diverges from the release tag and, on the second release, collides with the already-published version and fails the `npm publish` job (which blocks the downstream `github-release`). Add the opencode-v2 path to `version-sync.mjs` alongside this matrix entry.</comment>
<file context>
@@ -79,6 +79,8 @@ jobs:
workspace: "@cortexkit/opencode-antigravity-auth"
- package: "@cortexkit/pi-antigravity-auth"
workspace: "@cortexkit/pi-antigravity-auth"
+ - package: "@cortexkit/opencode-v2-antigravity-auth"
+ workspace: "@cortexkit/opencode-v2-antigravity-auth"
steps:
</file context>
There was a problem hiding this comment.
Fixed in b9136d9: scripts/version-sync.mjs now includes packages/opencode-v2/package.json in packageJsonPaths, so release tags bump the adapter version together with core/opencode/pi and the publish matrix cannot collide with a stale hardcoded version.
|
All eight review comments are addressed in 0660034 (push
Verified locally before pushing: |
The path allowlist used path.resolve() but only imported { join } from
node:path, so the document tool would throw a ReferenceError on every call
in a runtime without an ambient path global. Import resolve explicitly.
- P1: enforce the document boundary on the real path too — symlinks and junctions inside an allowed root could bypass the check because readFile follows them; realpath() is resolved and re-checked before reading - P2: add packages/opencode-v2/package.json to scripts/version-sync.mjs so release tags actually bump the adapter version (the publish matrix alone would publish a stale hardcoded 2.1.0 and collide on the second release) - P3: node --check only validates its first argument, so oauth-callback.mjs was never checked — typecheck scripts now run the check per file - P3: package test script uses bun test --isolate (repo convention) via the root composition that the bundled test runner accepts
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
… check A configured root that is itself a symlink or Windows junction would never match the resolved document path, rejecting every file under it. Roots are now canonicalized (realpathSync, lexical fallback) so junction-configured roots stay usable while the boundary is still enforced on the real path.
ualtinok
left a comment
There was a problem hiding this comment.
I re-audited the current head (ccd60d9) against the existing OpenCode adapter invariants and exercised the actual packages/opencode-v2/src/plugin.mjs request/response path with only the external account/transport collaborators stubbed. The existing repository gates pass, but the new adapter needs revision before merge.
Required revisions
-
Preserve the established final-wire invariants.
buildEnvelope()currently leaves a trailingrole: "model"unchanged and preservestoolConfig.functionCallingConfig.mode: "AUTO". The harness observed both values on the final outbound envelope. Antigravity rejects model-ending requests, and native AGY traffic requiresmode: "VALIDATED". Reuse or extract the shared final-wire preparation path, then add regressions proving a trailing[Continue]user turn andVALIDATEDmode. -
Do not forward rejected empty attempts, and surface embedded SSE failures.
pipeStream()writes each terminal frame before the caller decides whether the attempt was empty. A terminal-only response therefore produced three upstream calls and three forwardedSTOPframes; the host can terminate on the first frame, making the retries ineffective. Separately, an HTTP-200 SSEerrorframe on the non-streaming path produced three upstream calls followed by HTTP 200{"candidates":[]}. Buffer/classify empty attempts before exposing them, and treat embeddederror/promptFeedbackframes and candidate-less termination as failures rather than empty success. -
Enforce private filesystem permissions. A disk-level harness measured
0644forantigravity-v2.log,0755for the generated-image directory, and0644for a generated image. These files can contain local paths or generated user content. Match the existing adapters:0700parent directories,0600files, and repair permissions on existing paths. -
Dispose request-job timers. The ten-minute
setTimeout()created by the request hook is neither retained/cleared when the job is consumed norunref()'d. After completed requests and plugin cleanup, the harness process remained alive past 15 seconds until terminated. Store and clear each timer on consumption/cleanup, or unref it, and add a lifecycle regression. -
Retain the current account/retry state-machine semantics. The adapter currently treats all 403s as generic rate limits, ignores
retry-after-ms, and advances the endpoint fallback only on 404 rather than bounded capacity/server failures. That loses the existingACCOUNT_INELIGIBLE/validation-required transitions and capacity fallback behavior. This should use a shared core request engine rather than reimplementing a reduced retry loop. -
Make local-document access fail closed.
antigravity_read_documentdefaults to every supported document under the user's home directory, has no input-size bound, and calls Antigravity without the tool invocation's cancellation signal. Require explicitreadDocumentRoots(or disable the tool when absent), cap file size before buffering/base64 expansion, and propagate cancellation. -
Add behavior-level OpenCode 2 coverage and update the system-of-record docs. The package's only test checks the exported object shape, while its “typecheck” is syntax-only
node --check; none of the request, streaming, retry, OAuth, lifecycle, or filesystem behavior above is covered. Add a deterministic loopback E2E harness for the new adapter.ARCHITECTURE.md,STRUCTURE.md, andAGENTS.MDalso need the fourth published host adapter, its process/trust boundaries, and its release gates. Theengines.opencode: ">=2 <3"declaration should also account for the current0.0.0-beta-*host releases if this package is intended to ship before OpenCode 2 stable.
The host-hook/loopback approach is promising, but the 1,175-line adapter currently duplicates transformation, retry, streaming, OAuth, and artifact handling that already has hardened implementations elsewhere in this repository. I recommend extracting the reusable request engine into core and keeping this package as the thin OpenCode 2 composition layer before publishing it.
Title
Add an OpenCode 2.x host adapter (reuses the existing core, no changes to the 1.x package)
Background
OpenCode 2.x (
@opencode-ai/cli@0.0.0-beta-*, binaryopencode2) replaced the plugin surface:fetch()patching; insteadsession.hook("http.request" | "http.response");"package": "@opencode-ai/ai/providers/google";integration.transform, tools throughtool.transform;@cortexkit/opencode-antigravity-authdeclaresengines.opencode: ">=1.17.13 <2"and cannot load on 2.x (neither the TUI sidebar nor thefetchpatch exist there).So 2.x users currently have no Antigravity access. I wrote the host adapter; all logic still
comes from
@cortexkit/antigravity-auth-core@2.1.0— OAuth, transport, account pool, quota andthe model registry are untouched.
Implementation
session.hook("http.request")intercepts Antigravity model calls and rewrites the URL to aloopback server owned by the plugin.
Why a loopback server: 2.x sends whatever
event.requestthe hook leaves behind through itsown HTTP client, and
http.responseonly runs after that request succeeded, so a customResponsecannot be returned from the hook. The loopback keeps the core'sfetchWithAgyCliTransport(agy header order, proxy support) while the host still sees aplain, streamable, cancellable SSE.
getCurrentOrNextForFamily(hybrid) →refreshAntigravityToken→ensureProjectContext→buildAgyAgentRequestMetadata+orderAgyRequestPayloadInPlace→ANTIGRAVITY_ENDPOINT_FALLBACKS.429/403cools the account down viamarkRateLimitedWithReasonand rotates,401forces arefresh, and an empty-candidate
STOPis retried up to three times.@opencode-ai/ai/providers/googlepackage stays the codec, so image, PDF andtool-call handling comes from the host rather than a hand-written adapter.
Four issues found and worked around (may matter for 1.x / pi too)
strings, so
minLength: 1returns400 INVALID_ARGUMENT. Fixed withnormalizeGeminiTools(request, { moveNumericConstraintsToDescription: true })forgpt-*wire models (the flag already exists in core; it just needs to be enabled for that family).
contentand noparts, andClaude sometimes uses role
assistant; 2.x rejects both withInvalid google/gemini stream event. Frames must be normalised before forwarding.content-encoding: gzipto the host makes it inflate twice and fail.inlineDatain the request). I therefore register anantigravity_read_documenttool thatloads the local PDF/image itself and asks a multimodal model. Images pasted into chat work
natively. Generated images are written to disk and returned as a text path, because the 2.x
native parser only renders text and tool calls.
Verified (Windows 11 / Node 24 / OpenCode 0.0.0-beta-17595 / two-account pool)
Gemini 3.7 / 3.6 / 3.5 Flash, Gemini 3.1 Pro, Gemini 3.1 Flash Image,
Claude Sonnet 4.6 Thinking, Claude Opus 4.6 Thinking, GPT-OSS 120B;
tool; image generation written to disk; disabling account
#0failed over to#1.Privacy and credentials
#<account index>, upstream status codes and routing — no prompts, tokensor e-mail addresses.
antigravity-accounts.json(v4 schema + fenced lock),shared with the 1.x plugin and the standalone CLI.
How you may want to take it
packages/opencode-v2(or any name you prefer) and open a PR, includingthe bilingual README, an example config and the MIT licence;
split them into small PRs;
Whatever suits you. Thanks for the core — its transport and account-pool design made this
adapter straightforward.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Adds
@cortexkit/opencode-v2-antigravity-auth, a native OpenCode 2.x adapter that restores Antigravity access by rerouting Google model calls to a loopback that reuses the shared core. 2.x previously had no Antigravity support; 1.x behavior is unchanged. Non‑streamgenerateContentnow returns a merged JSON response, and generated images are saved to disk./models/*:(stream)generateContentviasession.hook("http.request"), forwards to127.0.0.1, unwraps SSE frames, normalizes events for the strict 2.x schema, retries bareSTOPup to 3 times, and merges non‑stream calls into one JSON response.@opencode-ai/ai/providers/googleas the codec; enablesnormalizeGeminiTools(..., { moveNumericConstraintsToDescription: true })for GPT‑OSS tool schemas.retry-after); endpoint fallbacks viaANTIGRAVITY_ENDPOINT_FALLBACKS.antigravity_read_documentfor PDFs/images; by default only files under the user’s home directory are readable, sensitive paths are always denied, the boundary is enforced on the real path (prevents symlink/junction escape), andreadDocumentRootsare canonicalized and can narrow access.antigravity-accounts.json, v4). Callback listens on127.0.0.1:51121/oauth-callback; the callback page only acknowledges receipt.<state dir>/antigravity-v2.logwithout prompts/tokens. Adds example config and tests; includes the package in the publish matrix andscripts/version-sync.mjs; typechecks both plugin and callback withnode --check; tests usebun test --isolate.resolveexplicitly in the path allowlist to avoid a runtimeReferenceError.Rollout
@cortexkit/antigravity-auth-core@2.1.0.opencode.jsonpointing tosrc/plugin.mjs(absolute path ornode_modules/@cortexkit/opencode-v2-antigravity-auth/src/plugin.mjs), and declare models under thegoogleprovider using@opencode-ai/ai/providers/google.googleintegration and select “Google Antigravity (add account)”; allow localhost access and port 51121.ANTIGRAVITY_ACCOUNTS_FILE; setreadDocumentRootsto restrict document access.Written for commit ccd60d9. Summary will update on new commits.